#region Copyright
/// <copyright>
/// Copyright (c) 2007 Ilia Shramko.
/// Released under the terms of the GNU General Public License.
/// </copyright>
#endregion

using System;
using System.Collections;
using System.Collections.Specialized;
using System.Collections.Generic;
using System.Diagnostics;
using System.Drawing;
using System.Drawing.Imaging;
using System.Globalization;
using System.IO;
using System.Text;
using System.Threading;

using Osh.Media;

namespace Osh.ProgDvb
{
	public class PlaybackMonitor : IProgramScreenshotProvider
	{
		private static string RootScreenshotFolder = "Screenshots";
		private static string ScreenshotFolderFormat = "yyyy'-'MM'-'dd' 'HH'-00-00'";
		private static string ScreenshotFileNameFormat = "yyyy'-'MM'-'dd' 'HH'-'mm'-'ss";
		private static string ProgDvbScreenshotFileNamePattern = "* - *.bmp";
		private static string SimpleGrabberScreenshotFileNameFormat = "Screenshot - {0}.bmp";

		private const int MaxScreenshotCreationTime = 1000; // ms
		private const int TakeScreenshotInterval = 1000; // ms
		private const int SaveScreenshotInterval = 5000; // ms

		private ProgDvbApplication _progDvb;
		private ChannelId _channelId = ChannelId.Invalid;
		
		private TimeSpan _screenshotInterval = TimeSpan.FromSeconds(5);
		private Size _screenshotSize = new Size(160, 120);

		private System.Threading.Timer _takeScreenshotTimer;
		private System.Threading.Timer _saveScreenshotTimer;

		private ScreenshotMap _screenshotMap;
		private DateTime? _lastSavedScreenshotTime;
		private DateTime? _lastTakenScreenshotTime;

		#region ScreenshotMap
		private class ScreenshotMap
		{
			private const int ExtraCapacity = 100;
			private int _interval = 0;
			private DateTime?[] _map = new DateTime?[0];

			public ScreenshotMap(TimeSpan interval)
			{
				if(interval.TotalMilliseconds <= 0)
					throw new ArgumentOutOfRangeException("interval");
				_interval = (int)interval.TotalMilliseconds;
			}

			public int Count
			{
				get { return _map.Length; }
			}

			public bool ShouldTake(DateTime dateTime)
			{
				DateTime? intervalStartTime;
				DateTime? screenshotTime;
				if(GetIntervalInfo(dateTime, out intervalStartTime, out screenshotTime))
				{
					if(screenshotTime.HasValue &&
						screenshotTime.Value.Subtract(intervalStartTime.Value) <
						dateTime.Subtract(intervalStartTime.Value))
						return false;
				}
				return true;
			}

			private int GetIntervalIndex(DateTime dateTime)
			{
				if(_map.Length == 0)
					return 0;

				DateTime startTime = _map[0].Value;
				TimeSpan offset = dateTime.Subtract(startTime);
				double index = Math.Truncate(offset.TotalMilliseconds / _interval);
				if(index < int.MinValue)
					return int.MinValue;
				if(index > int.MaxValue)
					return int.MaxValue;
				return (int)index;
			}

			public DateTime? GetIntervalStartTime(DateTime dateTime)
			{
				if(_map.Length == 0)
					return null;

				DateTime startTime = _map[0].Value;
				TimeSpan offset = dateTime.Subtract(startTime);
				return startTime.AddMilliseconds(
					Math.Truncate(offset.TotalMilliseconds / _interval) * _interval);
			}

			public bool GetIntervalInfo(DateTime dateTime,
				out DateTime? intervalStartTime, out DateTime? screenshotTime)
			{
				intervalStartTime = null;
				screenshotTime = null;

				if(_map.Length == 0)
					return false;

				int index = GetIntervalIndex(dateTime);
				if(index < 0 || index >= _map.Length)
					return false;

				intervalStartTime = _map[0].Value.AddMilliseconds(index * _interval);
				screenshotTime = _map[index];

				//Trace.WriteLine(string.Format(
				//    "PlaybackMonitor.GetIntervalInfo(date: {0}, intervalStart: {1}, screenshot: {2})",
				//    dateTime, intervalStartTime, screenshotTime));

				return true;
			}

			public bool SetIfBetter(DateTime dateTime)
			{
				int index = GetIntervalIndex(dateTime);
				if(index < 0)
				{
					// Expand array and set an new item at 0 index
					DateTime?[] newArray = new DateTime?[_map.Length + (-index)];
					Array.Copy(_map, 0, newArray, -index, _map.Length);
					_map = newArray;
					_map[0] = dateTime;
				}
				else if(index < _map.Length)
				{
					// Replace existing item if it's close to the interval start time
					DateTime? intervalStartTime = GetIntervalStartTime(dateTime);
					if(intervalStartTime.HasValue)
					{
						if(_map[index].HasValue &&
							_map[index].Value.Subtract(intervalStartTime.Value) <=
							dateTime.Subtract(intervalStartTime.Value))
							return false;
					}
					_map[index] = dateTime;
					return true;
				}
				else // index >= _map.Length
				{
					// Expand the array and set a new item
					Array.Resize<DateTime?>(ref _map, index + ExtraCapacity);
					_map[index] = dateTime;
				}

				return true;
			}

			public void Clear()
			{
				_map = new DateTime?[0];
			}
		}
		#endregion ScreenshotMap

		public PlaybackMonitor(ProgDvbApplication progDvb)
		{
			if(progDvb == null)
				throw new ArgumentNullException("progDvb");

			_screenshotMap = new ScreenshotMap(_screenshotInterval);
			_progDvb = progDvb;
			_progDvb.MediaChange += new EventHandler<MediaChangeArgs>(ProgDvb_MediaChange);

			_channelId = progDvb.CurrentChannelId;

			DeleteScreenshots(progDvb.Settings.RecordFolders);

			//Start();
		}

		private void Start()
		{
			if(_takeScreenshotTimer != null)
				return;

			_saveScreenshotTimer = new Timer(new TimerCallback(this.OnSaveScreenshotTimer));
			_saveScreenshotTimer.Change(SaveScreenshotInterval, SaveScreenshotInterval);

			_takeScreenshotTimer = new Timer(new TimerCallback(this.OnTakeScreenshotTimer));
			_takeScreenshotTimer.Change(0, TakeScreenshotInterval);
		}

		private void Stop()
		{
			if(_takeScreenshotTimer == null && _saveScreenshotTimer == null)
				return;

			if(_takeScreenshotTimer != null)
			{
				lock(_takeScreenshotTimer)
				{
					if(_takeScreenshotTimer != null)
						_takeScreenshotTimer.Dispose();
					_takeScreenshotTimer = null;
				}
			}

			if(_saveScreenshotTimer != null)
			{
				lock(_saveScreenshotTimer)
				{
					if(_saveScreenshotTimer != null)
						_saveScreenshotTimer.Dispose();
					_saveScreenshotTimer = null;
				}
			}

			DeleteScreenshots(_progDvb.Settings.RecordFolders);
		}

		private void ProgDvb_MediaChange(object sender, MediaChangeArgs e)
		{
			_channelId = (e.Media as Channel).Id;
			DeleteScreenshots(_progDvb.Settings.RecordFolders);
			_lastSavedScreenshotTime = null;
		}

		private void ScanScreenshots(string[] recordFolders, ChannelId channelId)
		{
			_screenshotMap.Clear();
			foreach(string recordFolder in recordFolders)
			{
				string rootScreenshotFolder = GetChannelScreenshotFolder(recordFolder, channelId);
				string[] screenshots = Directory.GetFiles(rootScreenshotFolder, "*.*", SearchOption.AllDirectories);
				foreach(string screenshot in screenshots)
				{
					DateTime screenshotTime = GetSavedScreenshotTimeUtc(screenshot);
					screenshotTime = RoundScreenshotTimeUtc(screenshotTime);
					_screenshotMap.SetIfBetter(screenshotTime);
				}
			}
		}

		private void DeleteScreenshots(string[] recordFolders)
		{
			foreach(string recordFolder in recordFolders)
			{
				string folder = Path.Combine(recordFolder, RootScreenshotFolder);
				try
				{
					if(Directory.Exists(folder))
						Directory.Delete(folder, true);
				}
				catch(Exception) { }
			}
		}

		private void OnTakeScreenshotTimer(object stateInfo)
		{
			lock(_takeScreenshotTimer)
			{
				try
				{
					DateTime? currentTime = (_progDvb.Timeline as Timeline).CurrentState.CurrentTime;
					if(currentTime == null)
						return;
					DateTime? currentIntervalStartTime = _screenshotMap.GetIntervalStartTime(currentTime.Value);

					bool shouldTake = true;
					if(_lastTakenScreenshotTime.HasValue)
					{
						if(currentIntervalStartTime == null)
						{
							// If we get here, then the first screenshot was taken but was not saved yet for some reason.
							// Give some time to save the first screenshot before trying again.
							TimeSpan timePassed = currentTime.Value.Subtract(_lastTakenScreenshotTime.Value);
							if(Math.Abs(timePassed.TotalMilliseconds) < SaveScreenshotInterval)
								shouldTake = false;
						}
						else if(currentIntervalStartTime ==
							_screenshotMap.GetIntervalStartTime(_lastTakenScreenshotTime.Value))
							shouldTake = false;
					}

					if(shouldTake)
						shouldTake = _screenshotMap.ShouldTake(currentTime.Value);

					if(shouldTake)
					{
						TakeScreenshot();
						_lastTakenScreenshotTime = currentTime;
					}

					//Trace.WriteLine(string.Format(
					//    "PlaybackMonitor.OnTakeScreenshotTimer(now: {0}, cur: {1}, start: {2}, take: {3})",
					//    DateTime.UtcNow, currentTime, currentIntervalStartTime, shouldTake));
				}
				catch(Exception)
				{
					//Program.ProcessException(e);
				}
			}
		}

		private void TakeScreenshot()
		{
			string recordFolder = _progDvb.Settings.GetRecordFolder();
			if(recordFolder != null)
			{
				IntPtr window = Win32.FindWindow(null, "SimpleGrabber_CmdParser");
				if(window != IntPtr.Zero)
				{
					string fileName = string.Format(SimpleGrabberScreenshotFileNameFormat,
							DateTime.Now.ToString(ScreenshotFileNameFormat));
					string filePath = Path.Combine(recordFolder, fileName);
					string command = string.Format("Screenshot:{0}", filePath);
					Win32.SendMessage(window, Win32.WM_SETTEXT, IntPtr.Zero, command);
					return;
				}
				else
					_progDvb.TakeScreenshot();
			}
		}

		private void OnSaveScreenshotTimer(object stateInfo)
		{
			lock(_saveScreenshotTimer)
			{
				try
				{
					SaveScreenshots();
				}
				catch(Exception)
				{
					//Program.ProcessException(e);
				}
			}
		}

		private void SaveScreenshots()
		{

			if(this.CurrentChannelId == ChannelId.Invalid)
				return;

			string recordFolder = _progDvb.Settings.GetRecordFolder();
			if(recordFolder == null)
				return;

			string[] files = Directory.GetFiles(recordFolder,
				ProgDvbScreenshotFileNamePattern, SearchOption.TopDirectoryOnly);

			foreach(string file in files)
			{
				DateTime screenshotTime = GetScreenshotTimeUtc(file);
				screenshotTime = RoundScreenshotTimeUtc(screenshotTime);

				// Skip just taken screenshots, will grab them later.
				if(DateTime.UtcNow.Subtract(screenshotTime).TotalMilliseconds < MaxScreenshotCreationTime)
					continue;

				ITimelineInfo timelineInfo = (_progDvb.Timeline as Timeline).CaptureState();

				// Adjust screenshot time according to the current position of the timeline.
				if(timelineInfo.CurrentTimeUtc != null && timelineInfo.EndTimeUtc != null &&
					timelineInfo.CurrentTimeUtc < timelineInfo.EndTimeUtc)
				{
					screenshotTime -= timelineInfo.EndTimeUtc.Value.Subtract(timelineInfo.CurrentTimeUtc.Value);
					screenshotTime = RoundScreenshotTimeUtc(screenshotTime);
				}

				bool saveScreenshot = false;
				if(_lastSavedScreenshotTime == null)
				{
					if(screenshotTime >= timelineInfo.StartTimeUtc)
						saveScreenshot = true;
				}
				else
					saveScreenshot = true;

				if(saveScreenshot)
				{
					string destPath = GetScreenshotPath(recordFolder, this.CurrentChannelId, screenshotTime);
					string destFolder = Path.GetDirectoryName(destPath);
					if(!Directory.Exists(destFolder))
						Directory.CreateDirectory(destFolder);

					lock(_screenshotMap)
						saveScreenshot = _screenshotMap.SetIfBetter(screenshotTime);

					SaveScreenshot(file, destPath);
					_lastSavedScreenshotTime = screenshotTime;
				}

				//Trace.WriteLine(string.Format(
				//    "PlaybackMonitor.SaveScreenshots(sve: {0}, screenshotTime: {1})",
				//    saveScreenshot, screenshotTime));

				DeleteSourceScreenshot(file);
			}
		}

		private void SaveScreenshot(string screenshotPath, string destScreenshotPath)
		{
			byte[] bytes = File.ReadAllBytes(screenshotPath);
			FixBitmap(bytes);
			MemoryStream stream = new MemoryStream(bytes);
			Image image = Bitmap.FromStream(stream);
			image.Save(destScreenshotPath, ImageFormat.Jpeg);
		}

		/// <summary>
		/// Fixes ProgDVB screenshot capturing bug (VMR mode).
		/// At 0xA offset of a bitmap file there should be 0x36 value instead of 0x3A.
		/// </summary>
		/// <param name="bytes"></param>
		private void FixBitmap(byte[] bytes)
		{
			MemoryStream stream = new MemoryStream(bytes);
			using(BinaryReader reader = new BinaryReader(stream))
			{
				if(reader.ReadUInt16() == 0x4D42) // BM
				{
					// BITMAPFILEHEADER
					uint bmpFileSize = reader.ReadUInt32();
					reader.ReadUInt32(); // 2 reserved WORDs
					uint offBits = reader.ReadUInt32();
					// BITMAPINFOHEADER
					uint bmpInfoHeaderSize = reader.ReadUInt32(); // size

					uint bmpFileHeaderSize = 14;
					uint bmpFullHeaderSize = bmpInfoHeaderSize + bmpFileHeaderSize;
					if(bmpFullHeaderSize != offBits)
					{
						// Go to BITMAPFILEHEADER.bfOffBits field
						stream.Seek(-8, SeekOrigin.Current);
						using(BinaryWriter writer = new BinaryWriter(stream))
							writer.Write(bmpFullHeaderSize);
					}
				}
			}
		}

		private void DeleteSourceScreenshot(string path)
		{
			try
			{
				File.Delete(path);
			}
			catch {}
		}

		private ChannelId CurrentChannelId
		{
			get
			{
				if(_channelId == ChannelId.Invalid)
					_channelId = _progDvb.CurrentChannelId;
				return _channelId;
			}
		}

		private DateTime GetScreenshotTimeUtc(string path)
		{
			FileInfo fileInfo = new FileInfo(path);
			return fileInfo.CreationTimeUtc;
		}

		private DateTime RoundScreenshotTimeUtc(DateTime dt)
		{
			DateTime result = new DateTime(
				dt.Year, dt.Month, dt.Day, dt.Hour, dt.Minute, dt.Second, DateTimeKind.Utc);
			if(dt.Millisecond >= 500)
				result.AddSeconds(1);
			
			return result;
		}

		/// <summary>
		/// Extracts from the screenshot file name its creation date and time.
		/// </summary>
		/// <param name="path"></param>
		/// <returns></returns>
		private DateTime GetSavedScreenshotTimeUtc(string path)
		{
			string screenshotTimeStr = Path.GetFileNameWithoutExtension(path);
			return DateTime.ParseExact(
				path, ScreenshotFileNameFormat, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal);
		}

		private string GetChannelScreenshotFolder(string recordFolder, ChannelId channelId)
		{
			return Path.Combine(Path.Combine(recordFolder, RootScreenshotFolder), channelId.ToString());
		}

		private string GetChannelScreenshotFolder(
			string recordFolder, ChannelId channelId, DateTime screenshotTime)
		{
			string folder = GetChannelScreenshotFolder(recordFolder, channelId);
			return Path.Combine(folder, screenshotTime.ToString(ScreenshotFolderFormat));
		}

		private string GetScreenshotPath(string recordFolder, ChannelId channelId, DateTime screenshotTime)
		{
			string destFolder = GetChannelScreenshotFolder(recordFolder, channelId, screenshotTime);
			return Path.Combine(destFolder, screenshotTime.ToString(ScreenshotFileNameFormat) + ".jpg");
		}


		#region IProgramScreenshotProvider Members
		public bool Enabled
		{
			get { return _takeScreenshotTimer != null; }
			set
			{
				if(this.Enabled == value)
					return;

				if(value == true)
					Start();
				else
					Stop();
			}
		}

		public int ScreenshotInterval
		{
			get { return (int)_screenshotInterval.TotalMilliseconds; }
			set
			{
				if(value <= 0)
					throw new ArgumentOutOfRangeException("ScreenshotInterval");

				TimeSpan newInterval = TimeSpan.FromMilliseconds(value);
				if(newInterval == _screenshotInterval)
					return;
				_screenshotInterval = newInterval;

				lock(_screenshotMap)
					_screenshotMap = new ScreenshotMap(_screenshotInterval);

				if(_takeScreenshotTimer != null)
					lock(_takeScreenshotTimer)
						if(_takeScreenshotTimer != null)
							_takeScreenshotTimer.Change(0, TakeScreenshotInterval);
				

				if(_saveScreenshotTimer != null)
					lock(_saveScreenshotTimer)
						if(_saveScreenshotTimer != null)
							_saveScreenshotTimer.Change(SaveScreenshotInterval, SaveScreenshotInterval);
			}
		}

		public int ScreenshotWidth
		{
			get { return _screenshotSize.Width; }
			set
			{
				if(value <= 0)
					throw new ArgumentOutOfRangeException("ScreenshotWidth");
				_screenshotSize.Width = value;
			}
		}

		public int ScreenshotHeight
		{
			get { return _screenshotSize.Height; }
			set
			{
				if(value <= 0)
					throw new ArgumentOutOfRangeException("ScreenshotHeight");
				_screenshotSize.Height = value;
			}
		}

		public Size ScreenshotSize
		{
			get { return _screenshotSize; }
			set
			{
				if(value.Width <= 0 || value.Height <= 0)
					throw new ArgumentOutOfRangeException("ScreenshotSize");
				_screenshotSize = value;
			}
		}

		public string GetScreenshotPath(DateTime dateTimeUtc)
		{
			if(this.CurrentChannelId == ChannelId.Invalid)
				return null;

			string recordFolder = _progDvb.Settings.GetRecordFolder();
			if(recordFolder == null)
				return null;

			DateTime? intervalStartTime;
			DateTime? screenshotTime;
			if(!_screenshotMap.GetIntervalInfo(dateTimeUtc, out intervalStartTime, out screenshotTime) ||
				screenshotTime == null)
				return null;

			string destPath = GetScreenshotPath(recordFolder, this.CurrentChannelId, screenshotTime.Value);
			if(!File.Exists(destPath))
				return null;
			
			return destPath;
		}
		#endregion IProgramScreenshotProvider Members
	}
}
